package top.v5it.japi.plus.core.util;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.RSA;
import cn.hutool.crypto.asymmetric.Sign;
import cn.hutool.crypto.asymmetric.SignAlgorithm;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.extra.spring.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import top.v5it.japi.plus.common.context.AuthorizedContextHolder;
import top.v5it.japi.plus.common.util.AssertData;
import top.v5it.japi.plus.core.ConstantPool;
import top.v5it.japi.plus.core.cache.JapiCache;

import java.io.File;
import java.io.FileInputStream;
import java.io.UnsupportedEncodingException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Objects;

import static top.v5it.japi.plus.core.util.JapiSecureUtil.Key.*;

/**
 * 安全工具类
 *
 * @author zhanpu
 * @date 20-12-2
 */
@Slf4j
public class JapiSecureUtil {

    /**
     * 构造
     */
    private JapiSecureUtil() {
        throw new UnsupportedOperationException();
    }

    /**
     * 根据hasKey和接入号从redis中获取密钥
     *
     * @return aes加解密密码
     */
    public static String getAesKey() {
        return getByKeyChannel(AES_KEY.value);
    }

    /**
     * 根据hasKey和接入号从redis中获取密钥
     *
     * @param channel 接入号
     * @return 签名密钥
     */
    public static String getSignKey(String... channel) {
        return getByKeyChannel(SIGN_KEY.value, channel);
    }

    /**
     * 获取密钥
     *
     * @param channel 渠道号
     * @param field   字段
     * @return 密钥字条
     */
    public static String getByKeyChannel(String field, String... channel) {
        final String chnlid =
                StrUtil.isAllEmpty(channel)
                        ? AuthorizedContextHolder.getRequestAuthAttributes().getChannel()
                        : channel[0];

        final JapiCache cache = SpringUtil.getBean(JapiCache.class);
        final Object objV = cache.getKeyChannelInfo(field + ConstantPool.COLON + chnlid);
        AssertData.notBlankIfStr(objV
                , "1、接入渠道无效；\n2、或者系统缺失接入渠道信息配置，请检查数据库或缓存；");

        final Object objTime = cache.getKeyChannelInfo(END_TIME.value + ConstantPool.COLON + chnlid);
        final LocalDateTime endTime = LocalDateTime.parse(Objects.toString(objTime));

        if (LocalDateTime.now(ZoneId.systemDefault()).isAfter(endTime)) {
            log.warn("[{}]接入渠道已过期", chnlid);
        }

        return Objects.toString(objV);
    }

    /**
     * 验签
     *
     * @param signature 签名值
     * @param source    原数据
     * @param channel   渠道号
     * @return true-成功，false-失败
     */
    public static boolean hmacVerify(String signature, String source, String channel) {
        HMac hmac = SecureUtil.hmac(HmacAlgorithm.HmacSHA256, getSignKey(channel));
        String digestHex = hmac.digestHex(source);
        return StrUtil.equalsIgnoreCase(signature, digestHex);
    }

    //----验签-----------------------------------------------------------------------------------------------------------//

    /**
     * 私钥签名
     *
     * @param source  原数据
     * @param channel 渠道号
     * @return 签名值
     */
    public static String hmacSignature(String source) {
        HMac hmac = SecureUtil.hmac(HmacAlgorithm.HmacSHA256, getSignKey());
        return hmac.digestHex(source);
    }

    //----签名-----------------------------------------------------------------------------------------------------------//

    /**
     * 根据hasKey和证书序列号从redis中获取公钥
     *
     * @param serialNo 证书序列号
     * @return 公钥字符
     */
    public static String getPublicKey(String... serialNo) {
        String clientSerialNo = StrUtil.isAllEmpty(serialNo)
                ? AuthorizedContextHolder.getRequestAuthAttributes().getClientSerialNo()
                : serialNo[0];

        return getCertKey(false, clientSerialNo, PUBLIC_KEY.value);
    }

    /**
     * 根据hasKey和证书序列号从redis中获取私钥
     *
     * @return 私钥字符
     */
    public static String getPrivateKey() {
        return getCertKey(true, AuthorizedContextHolder.getRequestAuthAttributes().getServerSerialNo(), PRIVATE_KEY.value);
    }

    /**
     * 根据hasKey和证书序列号从redis中获取对称密钥
     *
     * @param serialNo 证书序列号
     * @return 对称密钥字符
     */
    private static String getSecretKey(String serialNo) {
        return getCertKey(false, serialNo, SECRET_KEY.value);
    }

    /**
     * 获取密钥
     *
     * @param isServer true服务端，false客户端
     * @param serialNo 证书序列号
     * @param field    字段
     * @return 密钥字条
     */
    public static String getCertKey(boolean isServer, String serialNo, String field) {

        final String endTimeKey = END_TIME.value + ConstantPool.COLON + serialNo;
        JapiCache japiCache = SpringUtil.getBean(JapiCache.class);
        Object certEndTimeObj = isServer ? japiCache.getServerCertInfo(endTimeKey) : japiCache.getClientCertInfo(endTimeKey);
        AssertData.notBlankIfStr(certEndTimeObj
                , "1、证书序列号无效；\n2、或者系统缺失证书配置，请检查数据库或缓存；");

        LocalDateTime endTime = LocalDateTime.parse(certEndTimeObj + "");
        AssertData.isTrue(LocalDateTime.now(ZoneId.systemDefault()).isBefore(endTime), "证书过期");
        final String key = field + ConstantPool.COLON + serialNo;
        Object cacheValueObj = isServer ? japiCache.getServerCertInfo(key) : japiCache.getClientCertInfo(key);
        AssertData.notBlankIfStr(cacheValueObj
                , "1、证书序列号无效；\n2、或者系统缺失证书配置，请检查数据库或缓存；");

        return Objects.toString(cacheValueObj);
    }

    /**
     * 默认随机生成128位AES对称密钥
     *
     * @return 对称密钥二进制
     */
    public static byte[] generateKey() {
        return generateKey(128);
    }

    //----生成对称密钥-----------------------------------------------------------------------------------------------------------//

    /**
     * 指定长度生成AES对称密钥
     *
     * @param keySize 密钥长度
     * @return 对称密钥二进制
     */
    public static byte[] generateKey(int keySize) {
        return SecureUtil.generateKey(SymmetricAlgorithm.AES.getValue(), keySize).getEncoded();
    }

    /**
     * 文件加密
     *
     * @param srcFilepath 原文件路径
     * @param tagFilepath 目标文件路径
     * @param secretKey   对称密钥字符
     */
    public static void aesEncryptFile(String srcFilepath, String tagFilepath, String secretKey) {
        aesEncryptFile(new File(srcFilepath), new File(tagFilepath), secretKey);
    }

    //----AES加密文件-----------------------------------------------------------------------------------------------------------//

    /**
     * 文件加密
     *
     * @param srcFilepath 原文件路径
     * @param tagFilepath 目标文件路径
     */
    public static void aesEncryptFile(String srcFilepath, String tagFilepath) {
        aesEncryptFile(srcFilepath, tagFilepath, getAesKey());
    }

    /**
     * 文件加密
     *
     * @param srcFile 原文件
     * @param tagFile 目标文件
     */
    public static void aesEncryptFile(File srcFile, File tagFile) {
        aesEncryptFile(srcFile, tagFile, getAesKey());
    }

    /**
     * 文件加密
     *
     * @param srcFile   原文件
     * @param tagFile   目标文件
     * @param secretKey 对称密钥字符
     */
    public static void aesEncryptFile(File srcFile, File tagFile, String secretKey) {
        aesEncryptFile(srcFile, tagFile, Base64.decode(secretKey));
    }

    /**
     * 文件加密
     *
     * @param srcFile 原文件
     * @param tagFile 目标文件
     * @param key     对称密钥二进制
     */
    public static void aesEncryptFile(File srcFile, File tagFile, byte[] key) {
        try (FileInputStream fileInputStream = IoUtil.toStream(srcFile)) {
            AES aes = SecureUtil.aes(key);
            byte[] encrypt = aes.encrypt(fileInputStream);
            FileUtil.writeBytes(encrypt, tagFile);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new IllegalStateException("文件加密失败");
        }
    }

    /**
     * 随机生成对称密钥进行文件加密，并返回密钥二进制
     *
     * @param srcFilepath 原文件路径
     * @param tagFilepath 目标文件路径
     * @return 对称密钥二进制
     */
    public static byte[] aesEncryptFileOutKeyBinary(String srcFilepath, String tagFilepath) {
        byte[] key = generateKey();
        aesEncryptFile(new File(srcFilepath), new File(tagFilepath), key);
        return key;
    }

    /**
     * 随机生成对称密钥进行文件加密，并返回密钥二进制
     *
     * @param srcFile 原文件
     * @param tagFile 目标文件
     * @return 对称密钥二进制
     */
    public static byte[] aesEncryptFileOutKeyBinary(File srcFile, File tagFile) {
        byte[] key = generateKey();
        aesEncryptFile(srcFile, tagFile, key);
        return key;
    }

    /**
     * 随机生成对称密钥进行文件加密，并返回密钥字符
     *
     * @param srcFilepath 原文件路径
     * @param tagFilepath 目标文件路径
     * @return 对称密钥字符
     */
    public static String aesEncryptFileOutKeyStr(String srcFilepath, String tagFilepath) {
        return Base64.encode(aesEncryptFileOutKeyBinary(srcFilepath, tagFilepath));
    }

    /**
     * 随机生成对称密钥进行文件加密，并返回密钥字符
     *
     * @param srcFile 原文件
     * @param tagFile 目标文件
     * @return 对称密钥字符
     */
    public static String aesEncryptFileOutKeyStr(File srcFile, File tagFile) {
        return Base64.encode(aesEncryptFileOutKeyBinary(srcFile, tagFile));
    }

    /**
     * 文件解密
     *
     * @param srcFilepath 原文件
     * @param tagFilepath 目标文件
     * @param secretKey   对称密钥
     */
    public static void aesDecryptFile(String srcFilepath, String tagFilepath, String secretKey) {
        aesDecryptFile(new File(srcFilepath), new File(tagFilepath), secretKey);
    }

    //----AES解密文件-----------------------------------------------------------------------------------------------------------//

    /**
     * 文件解密
     *
     * @param srcFilepath 原文件
     * @param tagFilepath 目标文件
     */
    public static void aesDecryptFile(String srcFilepath, String tagFilepath) {
        aesDecryptFile(srcFilepath, tagFilepath, getAesKey());
    }

    /**
     * 文件解密
     *
     * @param srcFile 原文件
     * @param tagFile 目标文件
     */
    public static void aesDecryptFile(File srcFile, File tagFile) {
        aesDecryptFile(srcFile, tagFile, getAesKey());
    }

    /**
     * 文件解密
     *
     * @param srcFile   原文件
     * @param tagFile   目标文件
     * @param secretKey 对称密钥字符
     */
    public static void aesDecryptFile(File srcFile, File tagFile, String secretKey) {
        aesDecryptFile(srcFile, tagFile, SecureUtil.decode(secretKey));
    }

    /**
     * 文件解密
     *
     * @param srcFile 原文件
     * @param tagFile 目标文件
     * @param key     对称密钥二进制
     */
    public static void aesDecryptFile(File srcFile, File tagFile, byte[] key) {
        try (FileInputStream fileInputStream = IoUtil.toStream(srcFile)) {
            AES aes = SecureUtil.aes(key);
            byte[] decrypt = aes.decrypt(fileInputStream);
            FileUtil.writeBytes(decrypt, tagFile);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new IllegalStateException("文件解密失败");
        }
    }

    /**
     * 非对称加密
     *
     * @param source   原数据
     * @return 密文字符
     */
    public static String rsaEncrypt(String source) {
        RSA rsa = SecureUtil.rsa(null, getPublicKey());
        return rsa.encryptBase64(source, KeyType.PublicKey);
    }

    //----RSA加密-----------------------------------------------------------------------------------------------------------//

    /**
     * 非对称解密
     *
     * @param source   原数据
     * @return 明文字符
     */
    public static String rsaDecrypt(String source) {
        return rsaDecrypt(source, getPrivateKey());
    }

    //----RSA解密-----------------------------------------------------------------------------------------------------------//

    /**
     * 非对称私钥解密
     *
     * @param source     原数据
     * @param privateKey 私钥字符
     * @return 明文字符
     */
    public static String rsaDecrypt(String source, String privateKey) {
        RSA rsa = SecureUtil.rsa(privateKey, null);
        return rsa.decryptStr(source, KeyType.PrivateKey);
    }

    /**
     * 对称加密
     *
     * @param source 原数据
     * @return 密文字符
     */
    public static String aesEncrypt(String source) {
        return aesEncrypt(source, getAesKey());
    }

    //----AES加密-----------------------------------------------------------------------------------------------------------//

    /**
     * 对称加密
     *
     * @param source    原数据
     * @param secretKey 对称密钥字条
     * @return 密文字符
     */
    public static String aesEncrypt(String source, String secretKey) {
        return aesEncrypt(source, SecureUtil.decode((secretKey)));
    }

    /**
     * 对称加密
     *
     * @param source 原数据
     * @param key    对称密钥二进制
     * @return 密文字符
     */
    public static String aesEncrypt(String source, byte[] key) {
        return SecureUtil.aes(key).encryptBase64(source);
    }

    /**
     * 对称解密
     *
     * @param source 原数据
     * @return 明文字符
     */
    public static String aesDecrypt(String source) {
        return aesDecrypt(source, getAesKey());
    }

    //----AES解密-----------------------------------------------------------------------------------------------------------//

    /**
     * 对称解密
     *
     * @param source    原数据
     * @param secretKey 对称密钥字条
     * @return 明文字符
     */
    public static String aesDecrypt(String source, String secretKey) {
        return aesDecrypt(source, SecureUtil.decode(secretKey));
    }

    /**
     * 对称解密
     *
     * @param source 原数据
     * @param key    对称密钥二进制
     * @return 明文字符
     */
    public static String aesDecrypt(String source, byte[] key) {
        return SecureUtil.aes(key).decryptStr(source);
    }

    /**
     * 验签
     *
     * @param signature 签名值
     * @param source    原数据
     * @param serialNo  证书序列号
     * @return true-成功，false-失败
     * @throws UnsupportedEncodingException
     */
    public static boolean sha256Verify(String signature, String source, String serialNo) throws UnsupportedEncodingException {
        Sign sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA, null, getPublicKey(serialNo));
        return sign.verify(source.getBytes(ConstantPool.CHARACTER_ENCODING), Base64.decode(signature));
    }

    //----验签-----------------------------------------------------------------------------------------------------------//

    /**
     * 私钥签名
     *
     * @param source   原数据
     * @return 签名值
     */
    public static String sha256Signature(String source) {
        return signature(source, getPrivateKey());
    }

    //----签名-----------------------------------------------------------------------------------------------------------//

    /**
     * 私钥签名
     *
     * @param source     原数据
     * @param privateKey 私钥字符
     * @return 签名值
     */
    public static String signature(String source, String privateKey) {
        Sign sign = SecureUtil.sign(SignAlgorithm.SHA256withRSA, privateKey, null);
        byte[] bytes = sign.sign(StrUtil.bytes(source, ConstantPool.CHARACTER_ENCODING));
        return Base64.encode(bytes);
    }

    enum Key {
        /**
         * aes加解密密码
         */
        AES_KEY("aesKey"),

        /**
         * 签名密钥
         */
        SIGN_KEY("signKey"),

        /**
         * 公钥
         */
        PUBLIC_KEY("publicKey"),

        /**
         * 私钥
         */
        PRIVATE_KEY("privateKey"),

        /**
         * 对称密钥
         */
        SECRET_KEY("secretKey"),

        /**
         * 结束时间
         */
        END_TIME("endTime");

        private final String value;

        Key(String value) {
            this.value = value;
        }
    }
}
